/**
 *
 * The MIT License
 *
 * Copyright (c) 2008 Board of Trustees, Leland Stanford Jr. University. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:

 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.

 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.macdadi.ks.client.views;

import java.util.ArrayList;

import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.*;
import com.gwtext.client.core.EventObject;
import com.gwtext.client.core.RegionPosition;
import com.gwtext.client.widgets.Button;
import com.gwtext.client.widgets.Panel;
import com.gwtext.client.widgets.TabPanel;
import com.gwtext.client.widgets.Window;
import com.gwtext.client.widgets.event.ButtonListenerAdapter;
import com.gwtext.client.widgets.layout.BorderLayout;
import com.gwtext.client.widgets.layout.BorderLayoutData;

import org.macdadi.core.client.auth.AuthService;
import org.macdadi.core.client.auth.AuthenticatedUser;
import org.macdadi.core.client.auth.AuthorizationException;
import org.macdadi.core.client.domain.DataService;
import org.macdadi.core.client.domain.DataWriteService;
import org.macdadi.core.client.domain.Option;
import org.macdadi.core.client.domain.ProjectInfo;
import org.macdadi.core.client.ui.DefaultItemEditor;
import org.macdadi.core.client.ui.EditableTree;
import org.macdadi.ks.client.View;
import org.macdadi.ks.client.ViewContainer;
import org.macdadi.ks.client.components.ObjectDetails;

/**
 * @author Ben Suter <ben.suter@gmail.com>
 */
public class Options extends View implements TreeListener {
	protected EditableTree tree;
	protected ArrayList expandedItems = new ArrayList();
	protected ProjectInfo project;
	protected ObjectDetails optionsDetailPanel;
	private static String instructionsString;
	private static String bugsString;
	Button instructionsButton;

	protected EditableTree.ItemEditor OptionEdit, OptionAdd, OptionRemove;

	public static ViewInfo init()
	{
		String info = "<p>Designers provide options</p>";

		return new ViewInfo("Options", info)
		{
			public View createInstance(ViewContainer container)
			{
				return new Options(container);
			}

			public String getColor()
			{
				return "#215E21";
			}
		};
	}

	public Options(final ViewContainer container)
	{
		this.container = container;
		project = container.getCurrentProject();

		HorizontalPanel mainPanel = new HorizontalPanel();
		mainPanel.setStyleName("macdadi-Options");
		mainPanel.setWidth("100%");
		
		VerticalPanel treePanel = new VerticalPanel();
		treePanel.setWidth("850");

		tree = new EditableTree();
		tree.addTreeListener(this);

		OptionEdit = new OptionEdit(updateTree, project);
		OptionAdd = new OptionAdd(expandSelectedThenUpdateTree, project);
		OptionRemove = new OptionRemove(updateTree, project);

		Panel space = new Panel();
		space.setHeight(20);
		space.setBorder(false);

		instructionsString = "<p> On this page, the user is given an Editable Tree. For each node you can: </p>" +
		"<p> ~~~ </p> " +
		"<p><b>   1) Edit the name of the node </b></p>" +
		"<p><b>   2) Remove the node and its subnodes </b></p>" +
		"<p><b>   3) Add a subnode </b></p>" +
		"<p> ~~~ </p> " +
		"<p> Remarks - To perform these options, simply click on the desired node and three widgets will appear (a plus, minus, and a check). The plus widget adds a node; the minus widget removes the nodes; and the check widget renames the node. </p>" ;
		bugsString = "<p> None ATM </p>" +
		"<p> ~~~ </p>" +
		"<p> Discover any bugs? Please contact me at: <i>jwei512@stanford.edu</i> </p>";

		HTML instructionsHTML = new HTML(instructionsString);

		TabPanel tabPanel = new TabPanel();  
		tabPanel.setActiveTab(0);

		Panel tab1 = new Panel();  
		tab1.setTitle("Instructions");  
		tab1.add(instructionsHTML);
		tab1.setAutoScroll(true);  

		Panel tab2 = new Panel();  
		tab2.setTitle("Current Bugs/Fixes");  
		tab2.setHtml(bugsString);  
		tab2.setAutoScroll(true);

		tabPanel.add(tab1);
		tabPanel.add(tab2);

		BorderLayoutData centerData = new BorderLayoutData(RegionPosition.CENTER);  
		centerData.setMargins(3, 0, 3, 3);

		final Window window = new Window();
		window.setTitle("Instructions for Options Tab");  
		window.setClosable(true);  
		window.setWidth(600);  
		window.setHeight(350);  
		window.setPlain(true); 
		window.setLayout(new BorderLayout());  
		window.add(tabPanel, centerData);
		window.setCloseAction(Window.HIDE);

		instructionsButton = new Button("Instructions");  
		instructionsButton.addListener(new ButtonListenerAdapter() {
			public void onClick(Button button, EventObject e) {
				window.show();
			} 
		});

		optionsDetailPanel = new ObjectDetails(1,"option",container.getCurrentAuthToken());

		treePanel.add(instructionsButton);
		treePanel.add(space);
		treePanel.add(tree);
		mainPanel.add(treePanel);
		mainPanel.add(optionsDetailPanel);
		
		initWidget(mainPanel);
	}

	public void onShow() {
		populateTree(project);
		AuthenticatedUser user = container.getCurrentUser();

		if ( user.isAuthorizedOperation(project.getId(), AuthService.TARGET_OPTIONS, AuthService.OPERATION_CREATE))
			tree.addItemEditor(OptionAdd);

		if ( user.isAuthorizedOperation(project.getId(), AuthService.TARGET_OPTIONS, AuthService.OPERATION_REMOVE))
			tree.addItemEditor(OptionRemove);

		if ( user.isAuthorizedOperation(project.getId(), AuthService.TARGET_OPTIONS, AuthService.OPERATION_UPDATE))
			tree.addItemEditor(OptionEdit);
	}

	public void onHide() {
		tree.stopEditing();
		tree.removeItemEditor(OptionAdd);
		tree.removeItemEditor(OptionRemove);
		tree.removeItemEditor(OptionEdit);
		tree.clear();
	}

	/**
	 * Add a new root item to the tree, whose label initially displays a "loading ..." message.
	 * Then it retrieves all the root-level options for this project and adds each one to the
	 * tree. As it does so, the children of these options are added recursively. See addChild().
	 * <p/>
	 * Finally the rootItem is expanded, so that the first level of options
	 * is always visible.
	 *
	 * @param project the project from which the tree domain should be pulled
	 */
	protected void populateTree(final ProjectInfo project) {
		final TreeItem rootItem = new TreeItem();
		rootItem.setText("Loading options for " + project.getName());
		tree.addItem(rootItem);

		DataService.App.getInstance().getOptions(project.getId(), container.getCurrentAuthToken(), new AsyncCallback() {
			public void onFailure(Throwable caught) {
				GWT.log("Failed to populate the options tree", caught);
				if (caught instanceof AuthorizationException) {
					rootItem.setText("Please login to view options for " + project.getName());
				}
			}

			public void onSuccess(Object result) {
				//rootItem.setText("Options for " + project.getName());
				rootItem.setHTML("<p><font size = 5><b> Project Options </b></font></p>");
				
				Option[] roots = (Option[]) result;
				for (int i = 0; i < roots.length; i++) {
					addChild(rootItem, roots[i], true, true);
				}

				/* Expand the first and second levels */
				rootItem.setState(true, true);
				for (int i = 0; i < rootItem.getChildCount(); i++ ) {
					rootItem.getChild(i).setState(true, true);
				}
			}
		});
	}

	/**
	 * Adds the given Option as a child TreeItem to the parent TreeItem.
	 * Optionally steps into the given Option's children and does the same there,
	 * etc. recursively.
	 *
	 * @param parent  the tree node to act as parent for the newly added option
	 * @param child   the option to be added as a child node to the tree
	 * @param recurse if true, will call this method for the child and each of its children, etc.
	 */
	protected void addChild(TreeItem parent, Option child, boolean recurse, boolean secondLevel) {
		TreeItem node = new TreeItem();
		if(secondLevel) {
			node.setHTML("<p><font size = 3><i>" + child.getName() + "</i></font></p>");
		} else {
			node.setHTML("<font size = 2>" + child.getName() + "</font>");
		}
		secondLevel = false;
		node.setUserObject(child);
		parent.addItem(node);

		if (recurse) {
			for (int i = 0; i < child.getNumChildren(); i++)
				addChild(node, child.getChild(i), true, secondLevel);
		}

		/* If this node, as identified by its option's id, was "expanded" in a
		 * previous incarnation of this tree, then expand it here again.
		 */
		if (expandedItems.contains(Integer.toString(child.getId())))
			node.setState(true, false);
	}


	public void onTreeItemSelected(TreeItem item) {

	}

	/**
	 * Remember the expansion state of this item, so that we can later
	 * return the tree to the same state when it is refreshed.
	 *
	 * @param item the item whose expanded/folded state changed
	 */
	public void onTreeItemStateChanged(TreeItem item) {
		/* Remember the expansion state for later tree refresh, based on option ID. */
		Option o;
		if (item.getUserObject() instanceof Option)
			o = (Option) item.getUserObject();
		else
			return;

		if (item.getState())
			expandedItems.add(Integer.toString(o.getId()));
		else
			expandedItems.remove(Integer.toString(o.getId()));
	}


	/**
	 * A command that refreshes the tree.
	 */
	protected Command updateTree = new Command() {
		public void execute() {
			tree.clear();
			populateTree(project);
		}
	};

	/**
	 * A command that first expands the selected tree item, and then
	 * refreshes the tree. This is typically used when a new child
	 * gets added to a node, so that the new child will be visible
	 * after the refresh.
	 */
	protected Command expandSelectedThenUpdateTree = new Command() {
		public void execute() {
			/* If the item being edited is not already expanded, "remember"
			 * it as expanded for after the refresh.
			 */
			TreeItem edited = tree.getSelectedItem();
			if (edited != null) {
				if (edited.getUserObject() instanceof Option) {
					Option o;
					o = (Option) edited.getUserObject();
					String id;
					id = Integer.toString(o.getId());
					if (!expandedItems.contains(id))
						expandedItems.add(id);
				}
			}
			updateTree.execute();
		}
	};


	/**
	 * Collects functionality common to all editors related to Option nodes.
	 */
	public abstract class OptionItemEditor extends DefaultItemEditor {
		protected ProjectInfo project;

		public OptionItemEditor(String iconPath, String title, Command onSuccess, ProjectInfo p) {
			super(iconPath, title, onSuccess);
			this.project = p;
		}

		protected Widget getInputWidget(Object userObject) {
			if (userObject == null || !(userObject instanceof Option))
				throw new IllegalArgumentException("userObject must be a valid Option");

			return configureWidget((Option) userObject);
		}

		protected abstract Widget configureWidget(final Option option);
	}

	/**
	 * Lets you edit the Option's name. Only matches to TreeItems
	 * whose userObject is an Option.
	 */
	public class OptionEdit extends OptionItemEditor {
		public OptionEdit(Command onSuccess, ProjectInfo p) {
			super(DefaultItemEditor.iconPathEdit, "Option", onSuccess, p);
		}

		public boolean matchesItem(TreeItem item) {
			/* Only display this for non-root items */
			return (item.getParentItem() != null);
		}

		protected Widget configureWidget(final Option option) {
			/* A panel for editing the name: text input, apply, cancel */
			VerticalPanel vpE = new VerticalPanel();
			final TextBox tbEdit = new TextBox();
			tbEdit.setVisibleLength(50);
			tbEdit.setText(option.getName());
			vpE.add(tbEdit);

			HorizontalPanel hpE = new HorizontalPanel();

			Button btnEditDo = new Button("Apply");
			btnEditDo.addListener(new ButtonListenerAdapter() {
				public void onClick(Button button, EventObject e) {
					String newName = tbEdit.getText().trim();
					updateOption(option, newName);
				}
			});
			hpE.add(btnEditDo);

			Button btnEditCancel = new Button("Reset");
			btnEditCancel.addListener(new ButtonListenerAdapter() {
				public void onClick(Button button, EventObject e) {
					tbEdit.setText(option.getName());
				}
			});
			hpE.add(btnEditCancel);
			vpE.add(hpE);

			return vpE;
		}

		protected void updateOption(final Option option, String newName) {
			if (newName.equals(option.getName()))
				return; /* No change was made */

			DataWriteService.App.getInstance().updateOption(project.getId(), option.getId(), newName, container.getCurrentAuthToken(), this);
		}
	}

	/**
	 * Lets you add a new option as a child of the current option,
	 * providing an input field for the name of the new option.
	 * <p/>
	 * If added to a TreeItem that does not contain an option as
	 * its userObject, then the new option is added as a root-level
	 * option, not as a sub-option.
	 * <p/>
	 * This editor matches any TreeItem.
	 */
	public class OptionAdd extends OptionItemEditor {
		public OptionAdd(Command onSuccess, ProjectInfo p) {
			super(DefaultItemEditor.iconPathAdd, "Add a Sub-Option", onSuccess, p);
		}

		public boolean matchesItem(TreeItem item) {
			return true;
		}

		protected Widget getInputWidget(Object userObject) {
			return configureWidget((Option) userObject);
		}

		protected Widget configureWidget(final Option option) {
			/* A panel for editing the name: text input, apply, cancel */
			VerticalPanel vpA = new VerticalPanel();
			final TextBox tbAdd = new TextBox();
			tbAdd.setVisibleLength(50);
			vpA.add(tbAdd);
			HorizontalPanel hpA = new HorizontalPanel();
			Button btnAddDo = new Button("Create");
			btnAddDo.addListener(new ButtonListenerAdapter() {
				public void onClick(Button button, EventObject e) {
					addChildOption(option, tbAdd.getText().trim());
				}
			});
			hpA.add(btnAddDo);

			Button btnAddCancel = new Button("Reset");
			btnAddCancel.addListener(new ButtonListenerAdapter() {
				public void onClick(Button button, EventObject e) {
					tbAdd.setText("");
				}
			});
			hpA.add(btnAddCancel);
			vpA.add(hpA);

			return vpA;
		}

		protected void addChildOption(final Option option, String newName) {
			if (newName.length() == 0) {
				displayError("You must enter a name for the new option", null);
				return;
			}

			int parent = -1;
			if (option != null)
				parent = option.getId();

			DataWriteService.App.getInstance().createOption(project.getId(), parent, newName, container.getCurrentAuthToken(), this);
		}
	}

	/**
	 * Displays a clickable link to remove the option and all its sub-options.
	 * <p/>
	 * This editor only matches TreeItem's whose userObject is an Option.
	 */
	public class OptionRemove extends OptionItemEditor {
		public OptionRemove(Command onSuccess, ProjectInfo p) {
			super(DefaultItemEditor.iconPathRemove, "Remove", onSuccess, p);
		}

		protected Widget configureWidget(final Option option) {
			HTML rem = new HTML("<u style='cursor:hand'>Remove this option (and all its sub-options)</u>");
			rem.addClickListener(new ClickListener() {
				public void onClick(Widget sender) {
					removeOptionAndChildren(option);
				}
			});

			return rem;
		}

		protected void removeOptionAndChildren(Option option) {
			DataWriteService.App.getInstance().removeOption(project.getId(), option.getId(), container.getCurrentAuthToken(), this);
		}
	}
}